package jp.co.nes.awssupport.report;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jp.co.nes.awssupport.access.dao.CampaignAssignedDao;
import jp.co.nes.awssupport.access.dao.CreditCouponAssignedDao;
import jp.co.nes.awssupport.access.dao.MiscellaneousExpensesAssignedDao;
import jp.co.nes.awssupport.access.dao.MonthlyCompiledBillingDao;
import jp.co.nes.awssupport.access.dao.PremiumSupportAssignedDao;
import jp.co.nes.awssupport.access.dao.SforceManageDao;
import jp.co.nes.awssupport.access.dao.TaxAssignedDao;
import jp.co.nes.awssupport.common.bean.ContractInformationBean;
import jp.co.nes.awssupport.common.bean.MonthlyCompiledBillingBean;
import jp.co.nes.awssupport.common.bean.PremiumSupportAssignedBean;
import jp.co.nes.awssupport.common.bean.TaxAssignedBean;
import jp.co.nes.awssupport.common.bean.UsageDetailsBean;
import jp.co.nes.awssupport.common.constant.SupportPlan;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;
import jp.co.nes.awssupport.common.utility.DateUtil;
import jp.co.nes.awssupport.common.utility.ExcelToPdfConverter;
import jp.co.nes.awssupport.report.constant.ReportType;
import jp.co.nes.awssupport.report.editor.IUsageReportEditor;
import jp.co.nes.awssupport.report.editor.UsageReportEditor;
import jp.co.nes.awssupport.report.utility.ReportHelper;

/**
 * ご利用明細を出力するクラスです。<br>
 * 作成したご利用明細(Excel, PDF)をs3にアップし、署名付きURLを作成します。<br>
 * また、作成した署名付きURLを、サポートポータルの利用実績へ反映します。
 */
public class UsageReportClient extends AbstractReportClient {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(UsageReportClient.class);

    /** ドメインの製品名、使用タイプの組み合わせ情報 */
    private DomainMapping domainMapping;

    /** 処理対象年月差月数(必須コマンドライン引数) */
    @Option(name = "-diffMonth", usage = "処理対象年月差月数(必須)", metaVar = "diffMonth", required = true)
    private String paramDiffMonth;

    /** Excelテンプレート内で定義されている「#DOCUMENT_TITLE#」と置換する文字列の末尾に付加する内容 */
    @Option(name = "-subjectSuffix", usage = "SubjectSuffix", metaVar = "subjectSuffix")
    private String paramSubjectSuffix;

    /** s3にアップロードする際に使用するバケット名 */
    private String bucketName;

    /** s3にアップロードする際に使用するフォルダ名(最後に/は不要)(%1$s:処理対象年月(uuuuMM)) */
    private String folderName;

    /** Excelテンプレートファイル(パス + ファイル名) */
    private String templateExcelFilePath;

    /** ご利用明細(Excel)を出力するディレクトリ(最後にセパレータは不要)(%1$s:処理対象年月(uuuuMM)) */
    private String excelOutputDirPath;

    /** ご利用明細(PDF)を出力するディレクトリ(最後にセパレータは不要)(%1$s:処理対象年月(uuuuMM)) */
    private String pdfOutputDirPath;

    /** Excelテンプレート内で定義されている「#DOCUMENT_TITLE#」と置換する文字列(%d(1つ目):処理対象年 %d(2つ目):処理対象月 %s:末尾に付加する内容) */
    private String subject;

    /** ご利用明細(Excel)のファイル名(%s(1つ目):連結アカウントID %s(2つ目):処理対象年月(uuuuMM)) */
    private String excelFileName;

    /** ご利用明細(PDF)のファイル名(%s(1つ目):連結アカウントID %s(2つ目):処理対象年月(uuuuMM)) */
    private String pdfFfileName;

    /** ポータルサイトに表示する内容(リンク名)(Excel)(%1$s:対象年月(uuuuMM) %2$s:有効期限) */
    private String linkExcelTitle;

    /** ポータルサイトに表示する内容(リンク名)(PDF)(%1$s:対象年月(uuuuMM) %2$s:有効期限) */
    private String linkPdfTitle;

    /**
     * メインメソッドです。
     *
     * @param args コマンドライン引数
     */
    public static void main(String[] args) {
        UsageReportClient client = new UsageReportClient();
        client.execute(args);
    }

    /**
     * デフォルトコンストラクタです。
     */
    public UsageReportClient() {
    }

    /**
     * レポートヘルパーを引数に持つコンストラクタです。
     *
     * @param reportHelper レポートヘルパー
     */
    public UsageReportClient(ReportHelper reportHelper) {
        super(reportHelper);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#getProcessName()
     */
    @Override
    protected String getProcessName() {
        return "ご利用明細出力処理";
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#getReportType()
     */
    @Override
    protected ReportType getReportType() {
        return ReportType.ExcelPdf;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#getDiffMonth()
     */
    @Override
    protected long getDiffMonth() {
        return Long.parseLong(paramDiffMonth);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#getBucketName()
     */
    @Override
    protected String getBucketName() {
        return bucketName;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#getProcessedFolderName()
     */
    @Override
    protected String getFolderName() {
        return folderName;
    }

    /**
     * Excelテンプレートファイル(パス + ファイル名)を返却します。
     *
     * @return Excelテンプレートファイル(パス + ファイル名)
     */
    private String getTemplateExcelFilePath() {
        return templateExcelFilePath;
    }

    /**
     * ご利用明細(Excel)を出力するディレクトリ(最後にセパレータは不要)(%1$s:処理対象年月(uuuuMM))を返却します。
     *
     * @return ご利用明細(Excel)を出力するディレクトリ(最後にセパレータは不要)(%1$s:処理対象年月(uuuuMM))
     */
    private String getExcelOutputDirPath() {
        return excelOutputDirPath;
    }

    /**
     * ご利用明細(PDF)を出力するディレクトリ(最後にセパレータは不要)(%1$s:処理対象年月(uuuuMM))を返却します。
     *
     * @return ご利用明細(PDF)を出力するディレクトリ(最後にセパレータは不要)(%1$s:処理対象年月(uuuuMM))
     */
    private String getPdfOutputDirPath() {
        return pdfOutputDirPath;
    }

    /**
     * Excelテンプレート内で定義されている「#DOCUMENT_TITLE#」と置換する文字列(%d(1つ目):処理対象年 %d(2つ目):処理対象月)を返却します。
     *
     * @return Excelテンプレート内で定義されている「#DOCUMENT_TITLE#」と置換する文字列(%d(1つ目):処理対象年 %d(2つ目):処理対象月)
     */
    private String getSubject() {
        return subject;
    }

    /**
     * ご利用明細(Excel)のファイル名(%s(1つ目):連結アカウントID %s(2つ目):処理対象年月(uuuuMM))を返却します。
     *
     * @return ご利用明細(Excel)のファイル名(%s(1つ目):連結アカウントID %s(2つ目):処理対象年月(uuuuMM))
     */
    private String getExcelFileName() {
        return excelFileName;
    }

    /**
     * ご利用明細(PDF)のファイル名(%s(1つ目):連結アカウントID %s(2つ目):処理対象年月(uuuuMM))を返却します。
     *
     * @return ご利用明細(PDF)のファイル名(%s(1つ目):連結アカウントID %s(2つ目):処理対象年月(uuuuMM))
     */
    private String getPdfFfileName() {
        return pdfFfileName;
    }

    /**
     * ポータルサイトに表示する内容(リンク名)(Excel)(%1$s:対象年月(uuuuMM) %2$s:有効期限)を返却します。
     *
     * @return ポータルサイトに表示する内容(リンク名)(Excel)(%1$s:対象年月(uuuuMM) %2$s:有効期限)
     */
    private String getLinkExcelTitle() {
        return linkExcelTitle;
    }

    /**
     * ポータルサイトに表示する内容(リンク名)(PDF)(%1$s:対象年月(uuuuMM) %2$s:有効期限)を返却します。
     *
     * @return ポータルサイトに表示する内容(リンク名)(PDF)(%1$s:対象年月(uuuuMM) %2$s:有効期限)
     */
    private String getLinkPdfTitle() {
        return linkPdfTitle;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#getDispReportName(java.lang.String)
     */
    @Override
    protected String getDispReportName(String objectKey) {
        String dispReportName = null;
        if (objectKey.toUpperCase().matches(".*\\.PDF")) {
            // オブジェクトキーがPDFファイルの場合
            dispReportName = getLinkPdfTitle();
        } else {
            // オブジェクトキーがPDFファイル以外(Excel)の場合
            dispReportName = getLinkExcelTitle();
        }
        return dispReportName;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#getMethodNameForURL(java.lang.String)
     */
    @Override
    protected String getMethodNameForURL(String objectKey) {
        String methodNameForURL = null;
        if (objectKey.toUpperCase().matches(".*\\.PDF")) {
            // オブジェクトキーがPDFファイルの場合
            methodNameForURL = "setUsageDetailReportPDF";
        } else {
            // オブジェクトキーがPDFファイル以外(Excel)の場合
            methodNameForURL = "setUsageDetailReportXLS";
        }
        return methodNameForURL;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#initialize()
     */
    @Override
    protected void initialize() {
        super.initialize();
        domainMapping = new DomainMapping();

        String category = "UsageReport";
        bucketName = AppInfoUtil.getValAndEmptyChk(category, "BucketName");
        folderName = AppInfoUtil.getValAndEmptyChk(category, "FolderName");
        templateExcelFilePath = AppInfoUtil.getValAndEmptyChk(category, "TemplateExcelFilePath");
        excelOutputDirPath = AppInfoUtil.getValAndEmptyChk(category, "ExcelOutputDirPath");
        pdfOutputDirPath = AppInfoUtil.getValAndEmptyChk(category, "PdfOutputDirPath");
        subject = AppInfoUtil.getValAndEmptyChk(category, "Subject");
        excelFileName = AppInfoUtil.getValAndEmptyChk(category, "ExcelFileName");
        pdfFfileName = AppInfoUtil.getValAndEmptyChk(category, "PdfFileName");
        linkExcelTitle = AppInfoUtil.getValAndEmptyChk(category, "LinkExcelTitle");
        linkPdfTitle = AppInfoUtil.getValAndEmptyChk(category, "LinkPdfTitle");
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#searchContractInformationList(java.util.List)
     */
    @Override
    protected List<ContractInformationBean> searchContractInformationList(List<String> idList) {
        return SforceManageDao.getContractInformationList(idList, getTargetYearMonth());
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.report.creating.AbstractReportClient#createReport(java.lang.String, jp.co.nes.awssupport.access.bean.ContractInformationBean)
     */
    @Override
    protected boolean createReport(String targetId, ContractInformationBean ciBean) {
        if (logger.isDebugEnabled()) {
            logger.debug(toMessage(targetId, "createReport start"));
        }

        // 連結アカウントIDのチェック
        if (StringUtils.isEmpty(targetId)) {
            logger.error(toMessage("サポートポータルから連結アカウントIDが取得できませんでした。"));
            return false;
        }

        UsageDetailsBean reportDetail = searchReportDetail(targetId, ciBean);
        if (reportDetail == null) {
            return false;
        }

        boolean isError = false;
        try {
            File excelReportFile = writeReportFile(reportDetail);
            File pdfReportFile = convertPdf(targetId, excelReportFile);
            addExcelPdfReport(excelReportFile, pdfReportFile);
        } catch (IOException | EncryptedDocumentException | InvalidFormatException e) {
            // エラーが発生した場合、エラーログを出力し、処理を続行する。
            isError = true;
            logger.error(toMessage(targetId, "ご利用明細作成失敗"), e);
        }
        return !isError;
    }

    /**
     * レポートを出力する際に使用するデータを返却します。
     *
     * @param targetId 処理対象の連結アカウントID
     * @param ciBean 契約情報
     * @return レポートを出力する際に使用するデータ
     */
    private UsageDetailsBean searchReportDetail(String targetId, ContractInformationBean ciBean) {
        UsageDetailsBean usageDetailsBean = new UsageDetailsBean();
        usageDetailsBean.setAccountId(targetId);
        usageDetailsBean.setPublishDate(DateUtil.toDate(DateUtil.getNowOfZonedDateTime()));
        String subjectSuffix = "";
        if (paramSubjectSuffix != null) {
            subjectSuffix = paramSubjectSuffix;
        }
        usageDetailsBean.setDocumentTitle(String.format(getSubject(), getTargetYearMonth().getYear(),
                getTargetYearMonth().getMonthValue(), subjectSuffix));
        usageDetailsBean.setCompany(ciBean.getBillingPersonCompany());
        usageDetailsBean.setDepartment(ciBean.getBillingPersonDepartment());
        usageDetailsBean.setTitle(ciBean.getBillingPersonTitle());
        usageDetailsBean.setName(ciBean.getBillingPersonName());
        usageDetailsBean.setRate(ciBean.getDollerYen());

        // 月次集計結果テーブルから出力する内容を取得する。
        // なお、使用量、適用レートは小数第9位で四捨五入された結果、請求額（税抜）は、小数第4位で四捨五入された結果になっている前提。
        MonthlyCompiledBillingDao dao = new MonthlyCompiledBillingDao();
        List<MonthlyCompiledBillingBean> mcbList = dao.getListForUsageReport(targetId, getTargetYearMonth());
        if (mcbList.isEmpty()) {
            logger.warn(toMessage(targetId, "月次集計結果に該当データが存在しません。"));
            usageDetailsBean.setMonthlyCompiledBillingBeans(new ArrayList<>());
            usageDetailsBean.setMonthlyCompiledBillingBeansOfReserved(new ArrayList<>());
            usageDetailsBean.setMonthlyCompiledBillingBeansOfOther(new ArrayList<>());
            usageDetailsBean.setSumAmount1(0L);
            usageDetailsBean.setCampaignAssignedList(new ArrayList<>());
            usageDetailsBean.setCreditCouponAmountExist(false);
            usageDetailsBean.setCreditCouponAmount(0L);
            usageDetailsBean.setMiscellaneousExpensesAssignedList(new ArrayList<>());
            usageDetailsBean.setPremiumSupportAmountExist(false);
            usageDetailsBean.setPremiumSupportTargetAmount(0L);
            usageDetailsBean.setPremiumSupportAmount(0L);
            usageDetailsBean.setSumAmount2(0L);
            return usageDetailsBean;
        }

        // 取得した内容を従量利用分、リザーブド購入分、その他(ドメイン購入分など)に分割して、ご利用明細Beanに設定する。
        List<MonthlyCompiledBillingBean> meteredList = new ArrayList<>();
        List<MonthlyCompiledBillingBean> reservedList = new ArrayList<>();
        List<MonthlyCompiledBillingBean> otherList = new ArrayList<>();
        BigDecimal sumBillingAmount = new BigDecimal(0L);
        for (MonthlyCompiledBillingBean bean : mcbList) {
            // DBR版では、上記仕様だったが、CUR版では、このような状況が発生した連結アカウントIDだけをエラーにし、
            // 他の連結アカウントIDは処理を続行する仕様に変更する。
            BigDecimal billingAmount = bean.getBillingAmount();
            if (billingAmount == null) {
                // 過去の修正の関連(DBR版 -> CUR版 -> CURのみでの集計)から、請求額（税抜）のnullチェックを実施しているが、
                // 現状の請求額（税抜）は、月次集計結果テーブルでnull設定が不可なので、本エラーは、通常発生しない。
                logger.error(toMessage(targetId, "月次集計結果の請求額（税抜）が登録されていません。"));
                return null;
            }
            sumBillingAmount = sumBillingAmount.add(bean.getBillingAmount());

            if (bean.getUsageType() != null && bean.getUsageType().indexOf(":upfront") >= 0) {
                reservedList.add(bean);
                continue;
            }

            if (domainMapping.isDomain(bean.getProductName(), bean.getUsageType())) {
                otherList.add(bean);
                continue;
            }

            meteredList.add(bean);
        }
        usageDetailsBean.setMonthlyCompiledBillingBeans(meteredList);
        usageDetailsBean.setMonthlyCompiledBillingBeansOfReserved(reservedList);
        usageDetailsBean.setMonthlyCompiledBillingBeansOfOther(otherList);

        // 小計(税抜)の設定
        usageDetailsBean.setSumAmount1(sumBillingAmount.setScale(0, BigDecimal.ROUND_HALF_UP).longValueExact());

        // キャンペーンの設定
        CampaignAssignedDao caDao = new CampaignAssignedDao();
        usageDetailsBean.setCampaignAssignedList(
                caDao.getList(targetId, getTargetYearMonth()).stream().map(caBean -> {
                    caBean.setCampaignAmount(caBean.getCampaignAmount() * -1);
                    return caBean;
                }).collect(Collectors.toList()));

        // クレジットの設定
        CreditCouponAssignedDao ccaDao = new CreditCouponAssignedDao();
        boolean isCreditCouponAmountExist = false;
        long sumCCA = 0L;
        Long sumCCAL = ccaDao.getSumCreditCouponAmount(targetId, getTargetYearMonth());
        if (sumCCAL != null) {
            isCreditCouponAmountExist = true;
            sumCCA = sumCCAL.longValue() * -1;
        }
        usageDetailsBean.setCreditCouponAmountExist(isCreditCouponAmountExist);
        usageDetailsBean.setCreditCouponAmount(sumCCA);

        // 雑費の設定
        MiscellaneousExpensesAssignedDao meaDao = new MiscellaneousExpensesAssignedDao();
        usageDetailsBean.setMiscellaneousExpensesAssignedList(meaDao.getList(targetId, getTargetYearMonth()));

        // プレミアム料金の設定
        boolean isPremiumSupportAmountExist = false;
        long premiumSupportTargetAmount = 0L;
        long premiumSupportAmount = 0L;
        String supportPlan = ciBean.getSupportPlan();
        PremiumSupportAssignedDao psaDao = new PremiumSupportAssignedDao();
        PremiumSupportAssignedBean psaBean = psaDao.get(targetId, getTargetYearMonth());
        if (psaBean == null) {
            if (SupportPlan.PREMIUM.getSforceValue().equals(supportPlan)) {
                logger.error(toMessage(targetId, "プレミアムサポート料金が登録されていません。"));
                return null;
            }
        } else {
            if (!SupportPlan.PREMIUM.getSforceValue().equals(supportPlan)) {
                logger.error(toMessage(targetId, "プレミアムでありませんが、プレミアムサポート料金が登録されています。"));
                return null;
            }
            isPremiumSupportAmountExist = true;
            premiumSupportTargetAmount = psaBean.getTargetAmount();
            premiumSupportAmount = psaBean.getPremiumSupportAmount();
        }
        usageDetailsBean.setPremiumSupportAmountExist(isPremiumSupportAmountExist);
        usageDetailsBean.setPremiumSupportTargetAmount(premiumSupportTargetAmount);
        usageDetailsBean.setPremiumSupportAmount(premiumSupportAmount);

        // 税金適用の取得
        TaxAssignedDao taDao = new TaxAssignedDao();
        List<TaxAssignedBean> taxList = taDao.getList(targetId, getTargetYearMonth());
        if (taxList.isEmpty()) {
            logger.error(toMessage(targetId, "税金適用に該当データが存在しません。"));
            return null;
        }

        // 税金適用の課税対象額は、小数第1位で四捨五入して整数にした値が設定されている前提
        // →連結アカウントID, 年月では全て同じ値が入っている前提なので、先頭のものを使用する。
        long sumAmount2 = taxList.get(0).getTargetAmount().setScale(0, RoundingMode.HALF_UP).longValueExact();
        usageDetailsBean.setSumAmount2(sumAmount2);

        String errorMessage = check(usageDetailsBean);
        if (errorMessage != null) {
            logger.error(toMessage(targetId, errorMessage));
            return null;
        }

        return usageDetailsBean;
    }

    /**
     * ご利用明細に出力する内容のチェックです。<br>
     * チェックOKの場合は、null、NGの場合はエラーメッセージを返却します。
     *
     * @param usageDetailsBean ご利用明細Bean
     * @return チェックOKの場合は、null、NGの場合はエラーメッセージを返却します。
     */
    private String check(UsageDetailsBean usageDetailsBean) {
        long sumAount1 = usageDetailsBean.getSumAmount1();
        long sumCA = usageDetailsBean.getCampaignAssignedList().stream().mapToLong(caBean -> caBean.getCampaignAmount())
                .sum();
        long sumMEA = usageDetailsBean.getMiscellaneousExpensesAssignedList().stream()
                .mapToLong(meaBean -> meaBean.getMiscellaneousExpensesAmount()).sum();
        long cca = usageDetailsBean.getCreditCouponAmount();
        long psa = usageDetailsBean.getPremiumSupportAmount();
        long sumAmount2 = usageDetailsBean.getSumAmount2();

        // 請求額(税抜)の集計 - キャンペーン適用額 - クレジット適用額 + 雑費適用額 + プレミアムサポート適用額の合計(※1)が、
        // 税金適用テーブルの課税対象額が設定されているsumAmount2の同じかどうかチェックする。
        // なお、キャンペーン、クレジットは、マイナス値に変換されているので、以下の計算では「+」で良い。
        // (※1) 請求額(税抜)の集計 - キャンペーン適用額 - クレジット適用額が負の値の場合は0にする。
        //        請求額(税抜)の集計 - キャンペーン適用額 - クレジット適用額 + 雑費適用額 + プレミアムサポート適用額の負の値の場合は0にする。
        long value = sumAount1 + sumCA + cca;
        if (value < 0) {
            value = 0;
        }
        value = value + sumMEA + psa;
        if (value < 0) {
            value = 0;
        }

        if (value != sumAmount2) {
            StringBuilder builder = new StringBuilder();
            builder.append("計算結果不正 [小計(税抜):キャンペーン合計:クレジット:雑費合計:プレミアムサポート:ご使用料合計(税抜)][");
            builder.append(sumAount1);
            builder.append(":");
            builder.append(sumCA);
            builder.append(":");
            builder.append(cca);
            builder.append(":");
            builder.append(sumMEA);
            builder.append(":");
            builder.append(psa);
            builder.append(":");
            builder.append(sumAmount2);
            builder.append("]");
            return builder.toString();
        }
        return null;
    }

    /**
     * Excelファイルを書き込みます。
     *
     * @param usageDetailsBean ご利用明細
     * @return Excelファイルオブジェクト
     * @throws IOException IOエラーが発生した場合にスローします。
     * @throws InvalidFormatException ワークブックの形式でない場合にスローします。
     * @throws EncryptedDocumentException パスワードで保護されているワークブックの場合にスローします。
     */
    private File writeReportFile(UsageDetailsBean usageDetailsBean)
            throws IOException, EncryptedDocumentException, InvalidFormatException {
        String targetId = usageDetailsBean.getAccountId();

        logger.info(toMessage(targetId, "createUsageDetailsReport start"));

        // ご利用明細テンプレートを読み込む
        try (
                InputStream template = new FileInputStream(getTemplateExcelFilePath());
                Workbook workbook = WorkbookFactory.create(template)) {
            // テンプレートに値をセット
            IUsageReportEditor editor = createUsageReportEditor();
            editor.edit(workbook, usageDetailsBean);

            // ご利用明細ファイル出力
            String outputFileName = createOutputFileName(targetId, getExcelOutputDirPath(), getExcelFileName());
            File excelFile = new File(outputFileName);
            try (FileOutputStream outputStream = new FileOutputStream(outputFileName)) {
                workbook.write(outputStream);
                excelFile = new File(outputFileName);
                return excelFile;
            }
        }
    }

    /**
     * ご利用明細レポート編集インスタンスを生成します。
     *
     * @return ご利用明細レポート編集インスタンス
     */
    private IUsageReportEditor createUsageReportEditor() {
        return new UsageReportEditor();
    }

    /**
     * ExcelファイルオブジェクトをPDFファイルオブジェクトに変換します。
     *
     * @param targetId 連結アカウントID
     * @param excelReportFile Excelファイルオブジェクト
     * @return PDFファイルオブジェクト
     */
    private File convertPdf(String targetId, File excelReportFile) {
        logger.info(toMessage(targetId, "convertPdf start"));

        String category = "Report";
        String openOfficeRootPath = AppInfoUtil.getValAndEmptyChk(category, "OpenOfficeRootPath");
        int openOfficePort = AppInfoUtil.getIntValAndEmptyChk(category, "OpenOfficePort");

        File pdfReportFile = new File(createOutputFileName(targetId, getPdfOutputDirPath(), getPdfFfileName()));
        ExcelToPdfConverter converter = new ExcelToPdfConverter(openOfficeRootPath, openOfficePort);
        converter.convert(excelReportFile, pdfReportFile);
        return pdfReportFile;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractClient#debugArgument()
     */
    @Override
    protected void debugArgument() {
        logger.debug("diffMonth=" + paramDiffMonth);
        logger.debug("subjectSuffix=" + paramSubjectSuffix);
    }

    /**
     * ドメインの製品名、使用タイプの組み合わせ情報クラスです。
     */
    private static class DomainMapping {

        /**
         * ドメインの製品名、使用タイプの組み合わせ情報保持クラスです。
         */
        private class Mapping {

            /** 製品名 */
            public String productName;

            /** 使用タイプ */
            public String usageType;

            /**
             * デフォルトコンストラクタです。
             */
            public Mapping() {
            }
        }

        /** ドメインの製品名、使用タイプの組み合わせ情報リスト */
        private List<Mapping> mappingList = new ArrayList<>();

        /**
         * デフォルトコンストラクタです。<br>
         * ドメインの製品名、使用タイプの組み合わせ情報を読み込みます。
         */
        public DomainMapping() {
            String category = "DomainMappingForUsageReport";
            Map<String, String> appInfoMap = AppInfoUtil.getMap(category);
            Set<Entry<String, String>> entrySet = appInfoMap.entrySet();
            for (Entry<String, String> entry : entrySet) {
                Mapping mapping = new Mapping();
                mapping.productName = entry.getKey();
                mapping.usageType = entry.getValue();
                mappingList.add(mapping);
            }
        }

        /**
         * ドメインの製品名、使用タイプの組み合わせかどうかを返却します。
         *
         * @param productName 製品名
         * @param usageType 使用タイプ
         * @return true: ドメインの製品名、使用タイプの組み合わせ, false: ドメインの製品名、使用タイプの組み合わせでない。
         */
        public boolean isDomain(String productName, String usageType) {
            if (productName == null || usageType == null) {
                return false;
            }
            return mappingList.stream().anyMatch(
                    mapping -> productName.equals(mapping.productName) && usageType.equals(mapping.usageType));
        }
    }
}
